﻿using System.IO;
using System.Text;

namespace System.Net
{
    /// <summary>
    /// Net &amp; Web extensions.
    /// </summary>
    public static class Extensions
    {
        /// <summary>
        /// Executes GET Web-request.
        /// </summary>
        /// <param name="uri"><see cref="Uri">Uri</see> for web-request.</param>
        /// <param name="request">Request parameters (args).</param>
        /// <param name="processWebRequestAction"><see cref="Action{T1,T2}">Action</see> to be used for request processing.</param>
        /// <param name="failureAction"><see cref="Action{T1,T2}">Action to be executed in case of failure</see>.</param>
        /// <typeparam name="TRequest"></typeparam>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DoGetRequest<TRequest>(
            this Uri uri,
            TRequest request,
            Action<TRequest, WebRequest> processWebRequestAction,
            Action<TRequest, Exception> failureAction)
            where TRequest : class
        {
            if (processWebRequestAction == null)
            {
                throw new ArgumentNullException("processWebRequestAction");
            }

            try
            {
                var wrequest = WebRequest.CreateHttp(uri);

                wrequest.Method = "GET";

                processWebRequestAction(request, wrequest);
            }
            catch (Exception ex)
            {
                if (failureAction != null)
                {
                    failureAction(request, ex);
                }
            }
        }

        /// <summary>
        /// Executes POST Web-request.
        /// </summary>
        /// <param name="uri"><see cref="Uri">Uri</see> for web-request.</param>
        /// <param name="request">Request parameters (args).</param>
        /// <param name="writeParametersAction"><see cref="Action{T1,T2}">Action</see> that writes body-parameters.</param>
        /// <param name="processWebRequestAction"><see cref="Action{T1,T2}">Action</see> to be used for request processing.</param>
        /// <param name="failureAction"><see cref="Action{T1,T2}">Action to be executed in case of failure</see>.</param>
        /// <typeparam name="TRequest"></typeparam>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DoPostRequest<TRequest>(
            this Uri uri,
            TRequest request,
            Action<Stream, TRequest> writeParametersAction,
            Action<TRequest, WebRequest> processWebRequestAction,
            Action<TRequest, Exception> failureAction)
            where TRequest : class
        {
            try
            {
                var wrequest = WebRequest.CreateHttp(uri);

                wrequest.Method = "POST";

                wrequest.BeginGetRequestStream(
                    asyncResult =>
                        {
                            var writtenSuccesfully = true;

                            using (var stream = wrequest.EndGetRequestStream(asyncResult))
                            {
                                WritePostRequestParametersSafe(
                                    writeParametersAction, stream, request,
                                    (args, ex) =>
                                        {
                                            writtenSuccesfully = false;
                                            failureAction(args, ex);
                                        });
                            }

                            if (writtenSuccesfully)
                            {
                                processWebRequestAction(request, wrequest);
                            }
                        },
                    null);
            }
            catch (Exception ex)
            {
                if (failureAction != null)
                {
                    failureAction(request, ex);
                }
            }
        }

        /// <summary>
        /// Safely writes POST-request body parameters.
        /// </summary>
        /// <typeparam name="TArgs"></typeparam>
        /// <param name="writeParametersAction"><see cref="Action{T1,T2}">Action</see> that writes body-parameters.</param>
        /// <param name="stream"><see cref="Stream"/>.</param>
        /// <param name="request">Request arguments.</param>
        /// <param name="failureAction"><see cref="Action{T}">Action to be executed in case of failure</see>.</param>
        public static void WritePostRequestParametersSafe<TArgs>(
            Action<Stream, TArgs> writeParametersAction,
            Stream stream,
            TArgs request,
            Action<TArgs, Exception> failureAction)
            where TArgs : class
        {
            if (failureAction == null)
            {
                throw new ArgumentNullException("failureAction");
            }

            try
            {
                writeParametersAction(stream, request);
            }
            catch (Exception ex)
            {
                failureAction(request, ex);
            }
        }

        /// <summary>
        /// Processes Web-request response.
        /// </summary>
        /// <typeparam name="TRequest"></typeparam>
        /// <typeparam name="TResponse"></typeparam>
        /// <param name="response"><see cref="WebResponse"/>.</param>
        /// <param name="request">Request arguments.</param>
        /// <param name="successAction"><see cref="Action{T}">Action to be executed in case of success</see>.</param>
        /// <param name="failureAction"><see cref="Action{T}">Action to be executed in case of failure</see>.</param>
        /// <param name="converter">Converter to be used to extract response data in necessary format.</param>
        public static void ProcessWebRequestResponse<TRequest, TResponse>(
            this WebResponse response,
            TRequest request,
            Action<TRequest, TResponse> successAction,
            Action<TRequest, TResponse, Exception> failureAction,
            Func<WebResponse, TResponse> converter)
            where TRequest : class
            where TResponse : class
        {
            if (response == null)
            {
                throw new ArgumentNullException("response");
            }

            if (successAction == null)
            {
                throw new ArgumentNullException("successAction");
            }

            if (failureAction == null)
            {
                throw new ArgumentNullException("failureAction");
            }

            if (converter == null)
            {
                throw new ArgumentNullException("converter");
            }

            TResponse result = null;

            try
            {
                result = converter(response);

                successAction(request, result);
            }
            catch (Exception ex)
            {
                failureAction(request, result, ex);
            }
        }

        /// <summary>
        /// Writes parameters string to the <see cref="Stream">stream</see> using provided <see cref="Encoding">encoding</see>.
        /// </summary>
        /// <param name="stream"><see cref="Stream">Stream</see>.</param>
        /// <param name="parameters"><see cref="Func{TResult}">Function</see> that returns parameters string.</param>
        /// <param name="encoding"><see cref="Encoding">Encoding</see> (optional, default value will be UTF-8).</param>
        public static void WriteParameters(this Stream stream, Func<string> parameters, Encoding encoding = null)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            if (parameters == null)
            {
                return;
            }

            var tmp = parameters();

            var bytes = (encoding ?? Encoding.UTF8).GetBytes(tmp);

            stream.Write(bytes, 0, bytes.Length);
        }

        /// <summary>
        /// Executes Web request.
        /// </summary>
        /// <param name="request"><see cref="WebRequest">Web-request</see> to be executed.</param>
        /// <param name="successAction"><see cref="Action{T}">Action to be executed in case of success</see>.</param>
        /// <param name="failureAction"><see cref="Action{T}">Action to be executed in case of failure</see>.</param>
        /// <param name="encoding"><see cref="Encoding">Encoding</see> (optional, default value will be UTF-8).</param>
        public static void DoRequest(
            this WebRequest request,
            Action<byte[]> successAction,
            Action<Exception> failureAction,
            Encoding encoding = null)
        {
            request.BeginGetResponse(
                asyncResult =>
                    {
                        try
                        {
                            byte[] buffer;

                            using (var response = request.EndGetResponse(asyncResult))
                            {
                                buffer = response.Read(encoding ?? Encoding.UTF8);
                            }

                            successAction(buffer);
                        }
                        catch (Exception ex)
                        {
                            failureAction(ex);
                        }

                    }, null);
        }

        /// <summary>
        /// Reads <see cref="WebResponse">Web-response</see>.
        /// </summary>
        /// <param name="response"><see cref="WebResponse">Web-response</see>.</param>
        /// <param name="encoding"><see cref="Encoding">Encoding</see> (optional, default value will be UTF-8).</param>
        /// <returns>Byte-array.</returns>
        public static byte[] Read(this WebResponse response, Encoding encoding = null)
        {
            return !IsChunkedResponse(response)
                       ? ReadResponse(response, encoding ?? Encoding.UTF8)
                       : ReadChunkedResponse(response, encoding ?? Encoding.UTF8);
        }

        private static bool IsChunkedResponse(WebResponse response)
        {
            var header = response.Headers[HttpRequestHeader.TransferEncoding];

            return header != null && header.Equals("chunked", StringComparison.OrdinalIgnoreCase);
        }

        private static byte[] ReadResponse(WebResponse response, Encoding encoding = null)
        {
            var enc = encoding ?? Encoding.UTF8;
            var buffer = new byte[response.ContentLength];

            using (var stream = response.GetResponseStream())
            {
                stream.Read(buffer, 0, buffer.Length);
            }

            return buffer;
        }

        private static byte[] ReadChunkedResponse(WebResponse response, Encoding encoding = null)
        {
            var enc = (encoding ?? Encoding.UTF8);

            byte[] buffer;

            using (var streamReader = new StreamReader(response.GetResponseStream(), enc))
            {
                var content = new StringBuilder();

                while (!streamReader.EndOfStream)
                {
                    content.Append((char)streamReader.Read());
                }

                buffer = enc.GetBytes(content.ToString());
            }

            return buffer;
        }
    }
}
